package com.google.code.chaotic.client.ui;

import static com.google.code.chaotic.client.Constants.DEFAULT_MAP_CENTER;
import static com.google.code.chaotic.client.Constants.DEFAULT_ZOOM_LEVEL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import com.allen_sauer.gwt.log.client.Log;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.event.FxEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.fx.BaseEffect;
import com.extjs.gxt.ui.client.fx.FxConfig;
import com.google.code.chaotic.client.ClientSessionManager;
import com.google.code.chaotic.client.Util;
import com.google.code.chaotic.dto.AdminDto;
import com.google.code.chaotic.dto.CommuterDto;
import com.google.code.chaotic.dto.CommutingHabitDto;
import com.google.code.chaotic.dto.CommutingServiceDto;
import com.google.code.chaotic.dto.EndType;
import com.google.code.chaotic.dto.HabitInServiceDto;
import com.google.code.chaotic.dto.LatLonDto;
import com.google.code.chaotic.dto.ServiceWaypointDto;
import com.google.code.chaotic.dto.UserDto;
import com.google.code.chaotic.rpc.CommuterSvc;
import com.google.code.chaotic.rpc.CommuterSvcAsync;
import com.google.code.chaotic.rpc.JavaSvc;
import com.google.code.chaotic.rpc.JavaSvcAsync;
import com.google.code.chaotic.rpc.ServiceSvc;
import com.google.code.chaotic.rpc.ServiceSvcAsync;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.dom.client.Style.Float;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.Maps;
import com.google.gwt.maps.client.event.MapClickHandler;
import com.google.gwt.maps.client.geocode.DirectionQueryOptions;
import com.google.gwt.maps.client.geocode.DirectionResults;
import com.google.gwt.maps.client.geocode.Directions;
import com.google.gwt.maps.client.geocode.DirectionsCallback;
import com.google.gwt.maps.client.geocode.StatusCodes;
import com.google.gwt.maps.client.geocode.Waypoint;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.overlay.Icon;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.MarkerOptions;
import com.google.gwt.maps.client.overlay.Overlay;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.IncrementalCommand;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.StackLayoutPanel;

//

public class ServiceMgmtPanel extends Composite {

	public static final String DEFAULT_ICON_URL = "images/marker.png";
	public static final String DEFAULT_ICON_URL_HTTP = "http://maps.google.com/mapfiles/marker.png";

	// "http://maps.google.com/mapfiles/marker_orange.png";
	public static final String ORANGE_ICON_URL = "images/marker_orange.png";

	// "http://maps.google.com/mapfiles/marker_purple.png";
	public static final String PURPLE_ICON_URL = "images/marker_purple.png";

	// "http://maps.google.com/mapfiles/marker_green.png";
	public static final String GREEN_ICON_URL = "images/marker_green.png";

	// "http://maps.google.com/mapfiles/marker_grey.png";
	public static final String GREY_ICON_URL = "images/marker_grey.png";

	private List<CommutingServiceDto> services;

	/**
	 * Creating remote services' proxies to talk to the server-side services.
	 */
	private final JavaSvcAsync javaSvc = GWT.create(JavaSvc.class);
	private final CommuterSvcAsync commuterSvc = GWT.create(CommuterSvc.class);
	private final ServiceSvcAsync serviceSvc = GWT.create(ServiceSvc.class);

	private SplitLayoutPanel hSplitLayoutPanel = new SplitLayoutPanel();
	private DockLayoutPanel servicesPaginator = new DockLayoutPanel(Unit.EM);
	private PageNavigator pageNavigator = new PageNavigator();
	private StackLayoutPanel servicesStackPanel = new StackLayoutPanel(Unit.EM);

	private Image greenCheckImage = new Image("images/green-check.png");

	private MapWidget mapWidget = new MapWidget();
	// private DirectionsPanel directionsPanel = new DirectionsPanel();

	private SimplePanel sidePanel = new SimplePanel();

	//

	public ServiceMgmtPanel() {

		initWidget(hSplitLayoutPanel);

		servicesPaginator.addNorth(pageNavigator, 2);
		SimplePanel servicePage = new SimplePanel();
		servicePage.setWidget(servicesStackPanel);
		servicesPaginator.add(servicePage);
		sidePanel.setWidget(servicesPaginator);
		hSplitLayoutPanel.addWest(sidePanel, 200);

		servicesPaginator.setSize("100%", "100%");
		servicesStackPanel.setSize("100%", "100%");

		DeferredCommand.addCommand(new IncrementalCommand() {
			@Override
			public boolean execute() {
				if (Maps.isLoaded()) {
					initStackLayoutPanel();
					return false;
				} else
					return true;
			}
		});
		mapWidget.setSize("100%", "100%");
		hSplitLayoutPanel.add(mapWidget);

		mapWidget.setCenter(DEFAULT_MAP_CENTER);
		mapWidget.setZoomLevel(DEFAULT_ZOOM_LEVEL);
	}

	@Override
	protected void onLoad() {
		super.onLoad();
		pageNavigator.fillCurrentPage();
	}

	private void initStackLayoutPanel() {
		servicesStackPanel.addSelectionHandler(new SelectionHandler<Integer>() {
			@Override
			public void onSelection(SelectionEvent<Integer> event) {
				mapWidget.clearOverlays();
				Integer index = event.getSelectedItem();
				applyIndexToMap(index);
			}
		});
	}

	private void showOnePageOfServices(int pageNo, int fetchSize) {
		UserDto user = ClientSessionManager.getInstance().getUserNoPass();
		if (user instanceof AdminDto)
			serviceSvc.listAllServicesPaginated( //
					ClientSessionManager.getId(), pageNo, fetchSize,

					new AsyncCallback<List<CommutingServiceDto>>() {
						@Override
						public void onSuccess(List<CommutingServiceDto> services) {
							ServiceMgmtPanel.this.services = services;
							updateStackLayoutPanelItems(services);
						}

						@Override
						public void onFailure(Throwable caught) {
							Log.error(caught.toString(), caught);
						}
					});
		else if (user instanceof CommuterDto)
			serviceSvc.listOwnServicesPaginated( //
					ClientSessionManager.getId(), pageNo, fetchSize,

					new AsyncCallback<List<CommutingServiceDto>>() {
						@Override
						public void onSuccess(List<CommutingServiceDto> services) {
							ServiceMgmtPanel.this.services = services;
							updateStackLayoutPanelItems(services);
						}

						@Override
						public void onFailure(Throwable caught) {
							Log.error(caught.toString(), caught);
						}
					});
	}

	private void updateStackLayoutPanelItems(List<CommutingServiceDto> services) {
		servicesStackPanel.clear();
		for (final CommutingServiceDto service : services)
			createStackLayoutPanelItem(service);
	}

	private void createStackLayoutPanelItem(CommutingServiceDto service) {
		// FlowPanel servicePanel = new FlowPanel();
		ServicePanel servicePanel = new ServicePanel(service);
		servicePanel.setWidth("100%");
		servicePanel.setHeight("20em");
		servicesStackPanel.add(servicePanel,
				"<span style='font-size: 0.75em;'>Service no. "
						+ service.decodedId + "</span>", true, 2);
	}

	private SortData extractSortData( //
			CommutingServiceDto service, EndType endType) {

		List<HabitInServiceDto> hisList = service.getHabits();

		// Mapping: habitId -> HabitInServiceDto
		HashMap<String, HabitInServiceDto> hisMap // 
		= new HashMap<String, HabitInServiceDto>(hisList.size());

		List<HabitInServiceDto> sortedSublist // 
		= new LinkedList<HabitInServiceDto>();

		for (HabitInServiceDto his : hisList)
			hisMap.put(his.getHabitId(), his);

		List<ServiceWaypointDto> waypoints = service.getWaypoints();
		Collections.sort(waypoints, ServiceWaypointDto.SEQUENCE_COMPARATOR);

		// for (ServiceWaypointDto waypoint : waypoints) {
		// String habitId = waypoint.getCommutingHabitId();
		// if (habitId != null) {
		// HabitInServiceDto foundHis = hisMap.remove(habitId);
		// sortedSublist.add(foundHis);
		// }
		// }
		// TODO: A VEEERY DIRTY commenting!
		// Should be fixed as soon as possible!
		// DIRTY CODE BEGINS ...
		if (waypoints.size() >= hisList.size()) {
			for (int i = 0; i < hisList.size(); i++)
				sortedSublist.add(null);

			for (HabitInServiceDto his : hisList) {
				ServiceWaypointDto selectedWp = null;
				double minDistance = Double.MAX_VALUE;
				for (ServiceWaypointDto wp : waypoints) {
					CommutingHabitDto habit = his.getHabit();
					LatLonDto origOrDest //
					= endType == EndType.ORIG ? habit.getOrigin() //
							: habit.getDestination();
					LatLng point1 = Util.toLatLng(origOrDest);
					LatLng point2 = Util.toLatLng(wp.getPosition());
					double distance = point2.distanceFrom(point1);
					if (distance < minDistance) {
						minDistance = distance;
						selectedWp = wp;
					}
				}
				int index = selectedWp.getSequenceNo() - 1;
				if (endType == EndType.DEST)
					index -= hisList.size();
				sortedSublist.set(index, his);
			}
			hisMap.clear();
		}
		// DIRTY CODE ENDED!

		SortData data = new SortData();
		LinkedList<HabitInServiceDto> remainedSublist = new LinkedList<HabitInServiceDto>(
				hisMap.values());

		data.sortedBundles.addAll(toHabitPoints(sortedSublist, endType));
		data.unsortedBundles.addAll(toHabitPoints(remainedSublist, endType));
		return data;
	}

	public List<LatLng> extractPoints(List<HabitBundle> pairs) {

		ArrayList<LatLng> points = new ArrayList<LatLng>(pairs.size());
		for (HabitBundle pair : pairs)
			points.add(pair.point);

		return points;

	}

	public LinkedList<HabitBundle> toHabitPoints(
			List<HabitInServiceDto> hisList, EndType endType) {

		LinkedList<HabitBundle> pairs = new LinkedList<HabitBundle>();
		for (HabitInServiceDto his : hisList) {
			CommutingHabitDto habit = his.getHabit();
			pairs.add(new HabitBundle( //
					habit.getId(), //
					Util.toLatLng( //
							endType == EndType.ORIG ? habit.getOrigin() : habit
									.getDestination()),//
					his.getCancellationTime() != null));
		}
		return pairs;
	}

	public void markAll(List<LatLng> latLngs, String iconUrl) {

		MarkerOptions markerOption = MarkerOptions.newInstance();
		markerOption.setClickable(false);
		// Icon icon = Icon.newInstance(Icon.DEFAULT_ICON);
		Icon icon = Icon.newInstance(iconUrl);
		// Util.scaleIcon(icon, 0.75);
		markerOption.setIcon(icon);

		for (LatLng latLng : latLngs) {
			Marker marker = new Marker(latLng, markerOption);
			mapWidget.addOverlay(marker);
		}
	}

	public void markAllSorted(List<LatLng> latLngs, String color) {

		MarkerOptions markerOption = MarkerOptions.newInstance();
		markerOption.setClickable(false);
		StringBuilder commonSb = new StringBuilder(DEFAULT_ICON_URL_HTTP);
		commonSb.insert(commonSb.lastIndexOf("."), "_" + color);

		for (int i = 0; i < latLngs.size(); i++) {
			LatLng latLng = latLngs.get(i);
			StringBuilder sb = new StringBuilder(commonSb);
			sb.insert(sb.lastIndexOf("."), (char) ('A' + i));
			Icon icon = Icon.newInstance(sb.toString());
			// Util.scaleIcon(icon, 0.75);
			markerOption.setIcon(icon);
			Marker marker = new Marker(latLng, markerOption);
			mapWidget.addOverlay(marker);
		}
	}

	public void markAllClickable(List<HabitBundle> bundles, String iconUrl) {

		MarkerOptions markerOption = MarkerOptions.newInstance();
		markerOption.setClickable(true);
		markerOption.setDraggable(false);
		Icon icon = Icon.newInstance(iconUrl);
		// Util.scaleIcon(icon, 0.75);
		markerOption.setIcon(icon);

		for (HabitBundle bundle : bundles) {
			Marker marker = new Marker(bundle.point, markerOption);
			mapWidget.addOverlay(marker);
			bundle.marker = marker;
		}
	}

	public void markAllSortedClickable(List<HabitBundle> bundles, String color) {

		MarkerOptions markerOption = MarkerOptions.newInstance();
		markerOption.setClickable(true);
		markerOption.setDraggable(false);
		StringBuilder commonSb = new StringBuilder(DEFAULT_ICON_URL_HTTP);
		commonSb.insert(commonSb.lastIndexOf("."), "_" + color);

		for (int i = 0; i < bundles.size(); i++) {
			HabitBundle bundle = bundles.get(i);
			StringBuilder sb = new StringBuilder(commonSb);
			sb.insert(sb.lastIndexOf("."), (char) ('A' + i));
			Icon icon = Icon.newInstance(sb.toString());
			// Util.scaleIcon(icon, 0.75);
			markerOption.setIcon(icon);
			Marker marker = new Marker(bundle.point, markerOption);
			mapWidget.addOverlay(marker);
			bundle.marker = marker;
		}
	}

	private void applyIndexToMap(Integer index) {
		getElement().getStyle().setCursor(Cursor.WAIT);
		CommutingServiceDto service = services.get(index);
		LatLng serviceOrig = Util.toLatLng(service.getOrigin());
		LatLng serviceDest = Util.toLatLng(service.getDestination());

		ServicePanel servicePanel = (ServicePanel) //
		servicesStackPanel.getWidget(index);

		SortData origSortData = servicePanel.getOrigArrangeHandler().sortData;
		List<LatLng> sortedOrigs = extractPoints(origSortData.sortedBundles);
		List<LatLng> unsortedOrigs = extractPoints(origSortData.unsortedBundles);
		List<LatLng> cancelledOrigs = servicePanel.getOrigArrangeHandler().cancelledPoints;

		SortData destSortData = servicePanel.getDestArrangeHandler().sortData;
		List<LatLng> sortedDests = extractPoints(destSortData.sortedBundles);
		List<LatLng> unsortedDests = extractPoints(destSortData.unsortedBundles);
		List<LatLng> cancelledDests = servicePanel.getDestArrangeHandler().cancelledPoints;

		boolean allSorted = //
		(unsortedOrigs.isEmpty() //
				|| unsortedOrigs.size() == cancelledOrigs.size()) //
				&& (unsortedDests.isEmpty() //
				|| unsortedDests.size() == cancelledDests.size());
		String color = allSorted ? "green" : "purple";

		List<LatLng> sortedWaypoints = new LinkedList<LatLng>();
		sortedWaypoints.addAll(sortedOrigs);
		sortedWaypoints.addAll(sortedDests);
		markAllSorted(sortedWaypoints, color);

		markAll(unsortedOrigs, DEFAULT_ICON_URL);
		markAll(cancelledOrigs, GREY_ICON_URL);

		markAll(unsortedDests, DEFAULT_ICON_URL);
		markAll(cancelledDests, GREY_ICON_URL);

		servicePanel.setArrangementsEnabled(!service.isFinalized());
		servicePanel.setFinalizerEnabled(allSorted && !service.isFinalized());

		UserDto user = ClientSessionManager.getInstance().getUserNoPass();
		if (user instanceof CommuterDto) {
			CommuterDto commuter = (CommuterDto) user;
			HabitInServiceDto foundHis = null;
			outerFor: //
			for (CommutingHabitDto tempHabit : commuter.getCommutingHabits()) {
				List<HabitInServiceDto> hisList = service.getHabits();

				for (HabitInServiceDto his : hisList) {
					if (tempHabit.getId().equals(his.getHabitId())) {
						foundHis = his;
						break outerFor;
					}
				}
			}
			if (foundHis != null) {
				ArrayList<HabitBundle> bundles = new ArrayList<HabitBundle>(2);

				CommutingHabitDto habit = foundHis.getHabit();
				boolean cancelled = foundHis.getCancellationTime() != null;

				HabitBundle origBundle = new HabitBundle( //
						habit.getId(), //
						Util.toLatLng(habit.getOrigin()), //
						cancelled);
				HabitBundle destBundle = new HabitBundle( //
						habit.getId(), //
						Util.toLatLng(habit.getDestination()), //
						cancelled);
				bundles.add(origBundle);
				bundles.add(destBundle);
				markAllSortedClickable(bundles, !cancelled ? "purple" : "black");
			}
		}

		DirectionQueryOptions options = //
		new DirectionQueryOptions(mapWidget);

		// options.setPreserveViewport(true);
		Waypoint[] waypoints = null;
		if (allSorted) {
			waypoints = new Waypoint[sortedOrigs.size() * 2];
			int i = 0;
			for (LatLng latLng : sortedOrigs)
				waypoints[i++] = new Waypoint(latLng);
			for (LatLng latLng : sortedDests)
				waypoints[i++] = new Waypoint(latLng);

		} else {
			waypoints = new Waypoint[] //
			{ new Waypoint(serviceOrig), new Waypoint(serviceDest) };
		}

		Directions.loadFromWaypoints(waypoints, options,
				new MarkerHiderCallback());

		final ArrayList<LatLng> tempList = new ArrayList<LatLng>( //
				sortedOrigs.size() + unsortedOrigs.size()
						+ cancelledOrigs.size());
		tempList.addAll(sortedOrigs);
		tempList.addAll(unsortedOrigs);
		tempList.addAll(cancelledOrigs);
		tempList.addAll(sortedDests);
		tempList.addAll(unsortedDests);
		tempList.addAll(cancelledDests);

		mapWidget.checkResize();
		DeferredCommand.addCommand(new Command() {
			@Override
			public void execute() {
				Util.zoomToBounds(mapWidget, tempList);
			}
		});

		getElement().getStyle().setCursor(Cursor.DEFAULT);
	}

	//
	//
	//
	//
	//

	private final class PageNavigator extends DockLayoutPanel {
		private static final int FETCH_SIZE = 8;
		private int serviceCount = 0;
		private int currentPageNo = 1;
		private Label pageLabel = new Label("Page 1 of 10", false);

		private PageNavigator() {
			super(Unit.EM);
			Anchor prevAnch = new Anchor("Previous");
			Anchor nextAnch = new Anchor("Next");
			this.addWest(prevAnch, 5);
			this.addEast(nextAnch, 3);
			this.add(pageLabel);

			prevAnch.getElement().getStyle().setMargin(0.5, Unit.EM);
			nextAnch.getElement().getStyle().setMargin(0.5, Unit.EM);
			pageLabel.getElement().getStyle().setMargin(0.5, Unit.EM);

			prevAnch.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(ClickEvent event) {
					DeferredCommand.addCommand(new Command() {
						@Override
						public void execute() {
							if (currentPageNo < 2)
								return;
							currentPageNo--;
							changePageWithEffect();
						}
					});
				}

			});
			nextAnch.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(ClickEvent event) {
					DeferredCommand.addCommand(new Command() {
						@Override
						public void execute() {
							if (currentPageNo * FETCH_SIZE >= serviceCount)
								return;
							currentPageNo++;
							changePageWithEffect();
						}
					});
				}
			});
		}

		public void updatePageLabel() {
			int pageCount = (int) Math.ceil( //
					(double) serviceCount / FETCH_SIZE);
			pageLabel.setText("Page " + currentPageNo //
					+ " of " + pageCount);
		}

		private void changePageWithEffect() {
			final El el = new El(servicesStackPanel.getElement());
			final FxConfig fxConfig = new FxConfig(600);
			fxConfig.setEffectStartListener( //
					new Listener<FxEvent>() {
						@Override
						public void handleEvent(FxEvent be) {
							fillCurrentPage();
						}
					});
			fxConfig.setEffectCompleteListener(new Listener<FxEvent>() {
				@Override
				public void handleEvent(FxEvent be) {
					BaseEffect.fadeIn(el, new FxConfig(600));
				}
			});
			BaseEffect.fadeOut(el, fxConfig);
		}

		public void fillCurrentPage() {
			UserDto user = ClientSessionManager.getInstance().getUserNoPass();
			if (user instanceof AdminDto)
				serviceSvc.countAllServices( //
						ClientSessionManager.getId(),

						new AsyncCallback<Integer>() {
							@Override
							public void onSuccess(Integer result) {
								serviceCount = result;
								showOnePageOfServices(currentPageNo, FETCH_SIZE);
								new Timer() {
									@Override
									public void run() {
										updatePageLabel();
									}
								}.schedule(500);
							}

							@Override
							public void onFailure(Throwable caught) {
								Log.error(caught.toString(), caught);
							}
						});
			else if (user instanceof CommuterDto) {
				serviceSvc.countOwnServices( //
						ClientSessionManager.getId(),

						new AsyncCallback<Integer>() {
							@Override
							public void onSuccess(Integer result) {
								serviceCount = result;
								showOnePageOfServices(currentPageNo, FETCH_SIZE);
								new Timer() {
									@Override
									public void run() {
										updatePageLabel();
									}
								}.schedule(500);
							}

							@Override
							public void onFailure(Throwable caught) {
								Log.error(caught.toString(), caught);
							}
						});
			}
		}
	}

	private final class ServicePanel extends FlowPanel {

		private Anchor origArrangeAnch = new Anchor("Arrange");
		private Anchor destArrangeAnch = new Anchor("Arrange");
		private ArrangeAnchHandler origArrangeHandler;
		private ArrangeAnchHandler destArrangeHandler;
		private Anchor finalizeAnch = new Anchor("Finalize Arrangements");
		private DockLayoutPanel finalPanel = new DockLayoutPanel(Unit.EM);

		public ServicePanel(final CommutingServiceDto service) {
			UserDto user = ClientSessionManager.getInstance().getUserNoPass();

			DockLayoutPanel origPanel = new DockLayoutPanel(Unit.EM);
			DockLayoutPanel destPanel = new DockLayoutPanel(Unit.EM);
			DockLayoutPanel bothPanel = new DockLayoutPanel(Unit.EM);

			Label origLabel = new Label("Origins: ");
			Anchor origZoomAnch = new Anchor("Zoom");
			Label destLabel = new Label("Destinations: ");
			Anchor destZoomAnch = new Anchor("Zoom");

			Anchor bothZoomAnch = new Anchor("Fit to show both");

			class ZoomAnchHandler implements ClickHandler {
				private LatLng center;
				private final List<LatLng> points;

				public ZoomAnchHandler(LatLng center, List<LatLng> list) {
					this.center = center;
					this.points = list;
				}

				@Override
				public void onClick(ClickEvent event) {
					mapWidget.panTo(center);
					new Timer() {
						@Override
						public void run() {
							Util.zoomToBounds(mapWidget, points);
						}
					}.schedule(700);
				}
			}
			LatLng serviceOrig = Util.toLatLng(service.getOrigin());

			List<LatLng> allOrigs = Util.extractAllLatLngs(service,
					EndType.ORIG);
			origZoomAnch.addClickHandler(new ZoomAnchHandler( //
					serviceOrig, allOrigs));
			LatLng serviceDest = Util.toLatLng(service.getDestination());
			List<LatLng> allDests = Util.extractAllLatLngs(service,
					EndType.DEST);
			destZoomAnch.addClickHandler(new ZoomAnchHandler( //
					serviceDest, allDests));

			ArrayList<LatLng> fullList = new ArrayList<LatLng>(
					allOrigs.size() * 2);
			fullList.addAll(allOrigs);
			fullList.addAll(allDests);
			bothZoomAnch.addClickHandler(new ZoomAnchHandler( //
					Util.meanLatLng(Arrays.asList(serviceOrig, serviceDest)), //
					fullList));

			SortData origSortData = extractSortData(service, EndType.ORIG);
			SortData destSortData = extractSortData(service, EndType.DEST);

			this.origArrangeHandler = new ArrangeAnchHandler( //
					origSortData, //
					Util.extractLatLngs(service, EndType.ORIG, true));
			this.destArrangeHandler = new ArrangeAnchHandler( //
					destSortData, //
					Util.extractLatLngs(service, EndType.DEST, true));

			origArrangeAnch.addClickHandler(origArrangeHandler);
			destArrangeAnch.addClickHandler(destArrangeHandler);

			// origPanel.add(origLabel);
			origPanel.addWest(origZoomAnch, 6);
			if (user instanceof AdminDto)
				origPanel.add(origArrangeAnch);

			// destPanel.add(destLabel);
			destPanel.addWest(destZoomAnch, 6);
			if (user instanceof AdminDto)
				destPanel.add(destArrangeAnch);

			bothPanel.add(bothZoomAnch);

			finalizeAnch.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(ClickEvent event) {
					LinkedList<HabitBundle> sortedBundles = new LinkedList<HabitBundle>(
							origArrangeHandler.sortData.sortedBundles);
					sortedBundles
							.addAll(destArrangeHandler.sortData.sortedBundles);

					ArrayList<ServiceWaypointDto> waypointList = //
					new ArrayList<ServiceWaypointDto>(sortedBundles.size());

					for (int i = 0; i < sortedBundles.size(); i++) {
						HabitBundle bundle = sortedBundles.get(i);
						ServiceWaypointDto waypoint = new ServiceWaypointDto();
						waypoint.setCommutingService(service);
						waypoint.setSequenceNo(i + 1);
						waypoint.setPosition(Util.toLatLon(bundle.point));
						waypoint.setCommutingHabitId(bundle.habitId);
						waypointList.add(waypoint);
					}
					service.setWaypoints(waypointList);
					serviceSvc.finalizeService(ClientSessionManager.getId(),
							service.getId(), waypointList,
							new AsyncCallback<Void>() {
								@Override
								public void onSuccess(Void result) {
									service.setFinalized(true);
									setArrangementsEnabled(false);
									setFinalizerEnabled(false);
								}

								@Override
								public void onFailure(Throwable th) {
									Log.error(th.toString(), th);
								}
							});
				}
			});
			finalPanel.add(finalizeAnch);

			this.add(origLabel);
			this.add(origPanel);
			this.add(destLabel);
			this.add(destPanel);
			this.add(bothPanel);

			// origLabel.getElement().getStyle().setFloat(Float.LEFT);
			origZoomAnch.getElement().getStyle().setFloat(Float.RIGHT);
			origArrangeAnch.getElement().getStyle().setFloat(Float.RIGHT);

			// destLabel.getElement().getStyle().setFloat(Float.LEFT);
			destZoomAnch.getElement().getStyle().setFloat(Float.RIGHT);
			destArrangeAnch.getElement().getStyle().setFloat(Float.RIGHT);

			origLabel.getElement().getStyle().setMarginTop(1, Unit.EM);
			origPanel.getElement().getStyle().setMargin(0.5, Unit.EM);
			destPanel.getElement().getStyle().setMargin(0.5, Unit.EM);
			bothZoomAnch.getElement().getStyle().setMargin(0.5, Unit.EM);
			finalizeAnch.getElement().getStyle().setMargin(0.5, Unit.EM);

			finalPanel.getElement().getStyle().setBackgroundColor("#88FF00");

			origZoomAnch.getElement().getStyle().setMarginLeft(2, Unit.EM);
			origArrangeAnch.getElement().getStyle().setMarginLeft(2, Unit.EM);
			destZoomAnch.getElement().getStyle().setMarginLeft(2, Unit.EM);
			destArrangeAnch.getElement().getStyle().setMarginLeft(2, Unit.EM);

			origPanel.setSize("100%", "2em");
			destPanel.setSize("100%", "2em");
			bothPanel.setSize("100%", "2em");
			finalPanel.setSize("100%", "2em");
		}

		public ArrangeAnchHandler getOrigArrangeHandler() {
			return origArrangeHandler;
		}

		public ArrangeAnchHandler getDestArrangeHandler() {
			return destArrangeHandler;
		}

		public void setArrangementsEnabled(boolean enable) {
			origArrangeAnch.setVisible(enable);
			destArrangeAnch.setVisible(enable);
		}

		public boolean isArrangementsEnabled() {
			return origArrangeAnch.isVisible();
		}

		public void setFinalizerEnabled(boolean enable) {
			if (enable)
				this.add(finalPanel);

			else
				this.remove(finalPanel);
		}
	}

	private final class MarkerHiderCallback implements DirectionsCallback {
		@Override
		public void onSuccess(DirectionResults result) {
			final List<Marker> markers = result.getMarkers();
			for (final Marker marker : markers)
				DeferredCommand.addCommand(new Command() {
					@Override
					public void execute() {
						marker.setVisible(false);
					}
				});
		}

		@Override
		public void onFailure(int statusCode) {
			Log.error("Error in direction query! STATUS: "
					+ StatusCodes.getName(statusCode), new Exception(
					"Stack trace"));
		}
	}

	class ArrangeAnchHandler implements ClickHandler {
		final SortData sortData;
		final List<LatLng> cancelledPoints;

		public ArrangeAnchHandler(SortData data, List<LatLng> cancelledPoints) {
			this.sortData = data;
			this.cancelledPoints = cancelledPoints;
		}

		@Override
		public void onClick(ClickEvent event) {
			// mapWidget.clearOverlays();
			final LinkedList<HabitBundle> unsortedBundles = sortData.unsortedBundles;
			final LinkedList<HabitBundle> sortedBundles = sortData.sortedBundles;

			final List<LatLng> points = extractPoints(unsortedBundles);
			points.addAll(extractPoints(sortedBundles));
			mapWidget.panTo(Util.meanLatLng(points));
			new Timer() {
				@Override
				public void run() {
					Util.zoomToBounds(mapWidget, points);
				}
			}.schedule(700);
			mapWidget.addMapClickHandler(new MapClickHandler() {
				@Override
				public void onClick(MapClickEvent event) {
					Overlay overlay = event.getOverlay();
					if (overlay instanceof Marker) {
						Marker marker = (Marker) overlay;
						for (int i = sortedBundles.size() - 1; i >= 0; i--) {
							HabitBundle bundle = sortedBundles.get(i);
							if (bundle.marker == marker) {
								unsortedBundles.add(sortedBundles.remove(i));
								updateMarkersArrangement();
								return;
							}
						}
						for (int i = unsortedBundles.size() - 1; i >= 0; i--) {
							HabitBundle bundle = unsortedBundles.get(i);
							if (bundle.marker == marker) {
								sortedBundles.add(unsortedBundles.remove(i));
								updateMarkersArrangement();
								return;
							}
						}
					}
				}
			});

			updateMarkersArrangement();

			final FlowPanel vPanel = new FlowPanel();
			FlowPanel hPanel = new FlowPanel();
			Anchor backAnch = new Anchor("Back");

			backAnch.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(ClickEvent event) {
					sidePanel.remove(vPanel);
					sidePanel.setWidget(servicesPaginator);
					applyIndexToMap(servicesStackPanel.getVisibleIndex());
				}
			});

			Anchor resetAnch = new Anchor("Reset");
			resetAnch.addClickHandler(new ClickHandler() {
				@Override
				public void onClick(ClickEvent event) {
					unsortedBundles.addAll(sortedBundles);
					sortedBundles.clear();
					updateMarkersArrangement();
				}
			});
			// DirectionsPanel dirPanel = new DirectionsPanel();

			hPanel.add(backAnch);
			hPanel.add(resetAnch);

			vPanel.add(hPanel);
			// vPanel.add(dirPanel);

			hPanel.setWidth("100%");
			vPanel.setSize("100%", "100%");

			sidePanel.remove(servicesPaginator);
			sidePanel.setWidget(vPanel);

			backAnch.getElement().getStyle().setFloat(Float.LEFT);
			resetAnch.getElement().getStyle().setFloat(Float.LEFT);

			backAnch.getElement().getStyle().setMargin(.75, Unit.EM);
			resetAnch.getElement().getStyle().setMargin(.75, Unit.EM);
			resetAnch.getElement().getStyle().setMarginLeft(1.5, Unit.EM);
		}

		private void updateMarkersArrangement() {
			mapWidget.clearOverlays();
			markAllSortedClickable(sortData.sortedBundles, "purple");
			markAllClickable(sortData.unsortedBundles, DEFAULT_ICON_URL);
			markAll(cancelledPoints, GREY_ICON_URL);
		}
	}

	final class HabitBundle {
		String habitId;
		LatLng point;
		Marker marker;
		boolean cancelled;

		public HabitBundle(String habitId, LatLng point, boolean cancelled) {
			this.habitId = habitId;
			this.point = point;
			this.cancelled = cancelled;
		}
	}

	final class SortData {
		final LinkedList<HabitBundle> sortedBundles = new LinkedList<HabitBundle>();
		final LinkedList<HabitBundle> unsortedBundles = new LinkedList<HabitBundle>();
	}
}
